"""
虚拟机管理模块
"""
import json
import uuid
from typing import List
from uuid import uuid4

from flask import current_app, request
from flask_restx import abort, reqparse
from openstack import exceptions
from sqlalchemy import and_, or_
from werkzeug.exceptions import HTTPException
from tools.basic_tools import Tools
from app.api.v1.webhook.webhook_manager import WebHookManager
from template import l2_network_tpl, l3_network_tpl, vm_tpl, basic_tpl, router_tpl, eip_tpl, security_group_tpl

from app.api.v1.organizer.signal import red_blue
from app.common.range_manager import PrivateRangeException, RangeManager
from app.models.helpersModel import ApsChedulerJob
from app.models import (
    IssueInitialBindRelation,
    RangeVm,
    Switchs,
    User,
    coursewareModel,
    ctfModel,
    rangeModel,
    switchModel,
)
from app.api.v1.webhook.webhook_manager import WebHookManager
from tools.basic_tools import Tools
from app.models.ctfModel import CtfRedBlue
from app.models.rangeModel import RangeLinkModule
from app.models.roleModel import PresetRoleChoices
from configs.settings import flask_config
from tools import db
from utils.enums import TextChoices
from utils.utils import (
    base_detail,
    get_link_range_id,
    get_link_range_model,
    init_openstack,
    judge_topo,
    update_json_router_switch,
    update_uuid,
    revoke_task,
)

from tasks.tasks import add_job_to_scheduler, clean_range_vm, revoke_task
from .base_view import OrganizerBase
from app.common.transaction_manager import create_transaction


class RangeBase(OrganizerBase):
    @property
    def is_admin(self):
        return (
            self.user.is_superuser
            and request.cookies.get("role", None) == PresetRoleChoices.admin
        )

    def range_expand_filter(self, range_uuid, rang_id=None):
        """判断当前用户是否作为该场景关联比赛的导调/裁判角色"""
        if range_uuid:
            ctf_rb_filter = CtfRedBlue.query.filter(
                CtfRedBlue.range_uuid == range_uuid,
                or_(
                    CtfRedBlue.rb_pilot_id == self.user.id,
                    CtfRedBlue.rb_referee_id == self.user.id,
                ),
            )
        else:
            range_link = rangeModel.RangeLinkModule.query.filter_by(
                range_id=rang_id
            ).first()
            if not range_link:
                return False
            ctf_rb_filter = CtfRedBlue.query.filter(
                CtfRedBlue.range_uuid == range_link.range_uuid,
                or_(
                    CtfRedBlue.rb_pilot_id == self.user.id,
                    CtfRedBlue.rb_referee_id == self.user.id,
                ),
            )
        return db.exists(ctf_rb_filter)

    # 获取我的靶场 信息w
    def getMyRange(self, range_id=None):
        range_list = None
        if range_id:
            _filter = {"id": range_id}
            if not any(
                [
                    self.user.is_superuser,
                    self.range_expand_filter(None, rang_id=range_id),
                ]
            ):
                _filter["user_id"] = self.user.id
            range_list = rangeModel.Range.query.filter_by(**_filter).first()
            if not range_list:
                return None

        if not range_id:
            range_list = rangeModel.Range.query.filter_by(user_id=self.user.id).all()

        return range_list

    # 通过range_uuid获取靶场相关信息
    def getMyRangeByUUID(self, range_uuid):
        range_manager = RangeManager(range_uuid=range_uuid)
        return range_manager.range_model

    @staticmethod
    def checkRangeLinkModuleParm(_type, task_id=None, courseware_id=None, ctf_id=None):
        type_list = current_app.config["RANGE_CONF"]["RangeModule"]
        if _type not in type_list.keys():
            abort(400, message="参数错误")

        if vars()[type_list[_type]] is None:
            abort(400, message=f"{type_list[_type]}不能为空")
        return type_list[_type]

    # 获取靶场预设flag信息
    @staticmethod
    def get_range_preset_info(range_id):
        return ctfModel.CtfRedBlueFlag.query.filter_by(range_id=range_id).all()

    # def getRangeLinkModule(self, range_id,parm={}):
    #     rangeLinkModuleInfo = []
    #     type_list = current_app.config['RANGE_CONF']['RangeModule']
    #     if len(parm) > 1:
    #         abort(400, message="参数错误")
    #
    #     rangeLinkModuleInfo = rangeModel.RangeLinkModule.query.filter_by(range_id=range_id, user_id=self.user.id,  # noqa
    #                                                                          **parm).first()
    #     return rangeLinkModuleInfo

    # 获取系统靶场
    # def getSystemRange(self, range_id=None):
    #     if range_id:
    #         rangeList = rangeModel.Range.query.filter_by(id=range_id, is_system=1).all()
    #     # if rid and not uid:
    #     #     rangeList = rangeModel.MyRangeEnv.query.filter_by(rid=rid).all()
    #     if not range_id:
    #         rangeList = rangeModel.Range.query.filter_by(is_system=1).all()
    #
    #     return rangeList


# 靶场列表分页
class RangeList(RangeBase):
    @staticmethod
    def _paginate(paginate, data: list):
        if len(paginate.items) > 0:
            for content in paginate.items:
                print(content.rangeVmList)
                data_info = {
                    "name": (content.name,),
                    "topology_img": content.topology_img,
                    "describe": content.describe,
                    "start": content.start,
                    "range_template_type": content.range_template_type or "",
                    "is_public": content.is_public,
                    "create_user": User.query.get(
                        content.rangeVmList[0].user_id
                    ).account_name,
                }
                if len(content.rangeVmList) > 0:
                    range_model = content.rangeVmList[0]
                    # 判断是否存在关联赛题
                    issue_count = db.session.execute(
                        f"""
                        SELECT
                            COUNT(*)
                        FROM
                            api_vm_questions
                        JOIN api_range_vm ON api_range_vm.id = api_vm_questions.vm_id
                        JOIN api_range_module ON api_range_module.range_uuid = api_range_vm.range_uuid
                        AND api_range_module.id = {range_model.id}
                    """
                    )
                    data_info.update(
                        {
                            "range_uuid": range_model.range_uuid,
                            "has_issue": issue_count.scalar() > 0,
                        }
                    ),
                data.append(data_info)
        return data

    def get(self):
        params = request.args
        page = params.get("page", default=1, type=int)
        page_size = params.get("pageSize", default=10, type=int)
        system = params.get("system", type=int)
        hoveVm = params.get("hoveVm")
        keyword = params.get("keyword")
        is_link = params.get("is_link", type=int, default=0)

        if is_link not in (0, 1):
            abort(400, message="参数异常")

        if page_size >= 50:
            abort(400, message="每页数量不能超过50")

        if system not in [0, 1]:
            abort(400, message="参数错误")

        current_role_name = request.cookies.get("role", None)
        if not current_role_name:
            abort(400, message="无法获取当前登录角色")

        filters = [rangeModel.Range.is_system == system]
        if hoveVm:
            filters.append(rangeModel.Range.vmNum != 0)
        if keyword:
            # 添加关键词的检索选项
            filters.append(rangeModel.Range.name.contains(keyword))
        if is_link:
            range_ids = get_link_range_id()
            filters.append(rangeModel.Range.id.not_in(range_ids))
        try:
            # 导调只能查看绑定自己的红蓝对抗赛关联的场景
            if current_role_name == PresetRoleChoices.rb_pilot:
                bind_range_ids = [
                    item.range_id
                    for item in rangeModel.RangeLinkModule.query.join(
                        ctfModel.CtfRedBlue,
                        and_(
                            ctfModel.CtfRedBlue.rb_pilot_id == self.user.id,
                            ctfModel.CtfRedBlue.range_uuid
                            == rangeModel.RangeLinkModule.range_uuid,
                        ),
                    ).all()
                ]
                filters.append(rangeModel.Range.id.in_(bind_range_ids))
            elif not system:
                filters.append(
                    or_(
                        rangeModel.Range.user_id == self.user.id,
                        rangeModel.Range.is_public == 1,
                    ),
                )
            _select = rangeModel.Range.query.filter(*filters).order_by(
                rangeModel.Range.id.desc()
            )
            paginate = _select.paginate(page, page_size, error_out=False)
            info = {"current_page": page, "data": self._paginate(paginate, [])}
            info = base_detail(info, paginate)
            return {"code": 200, "data": info}

        except Exception as e:
            current_app.logger.exception(e)
            abort(500, message="参数错误")


# 靶场管理 基本信息 不包含虚机
class RangeManage(RangeBase):
    

    def get(self, range_uuid):
        if not range_uuid:
            abort(400, message="参数错误")

        # 暂定 同一个用户同一个靶场ID可以确定一个关联range_module_id
        res = self.getMyRangeByUUID(range_uuid=range_uuid)

        try:
            network = "ok" if res.network_id else ""
            info = [
                {
                    "range_id": res.range_id,
                    "range_uuid": res.range_uuid,
                    "name": res.rangeInfo[0].name,
                    "describe": res.rangeInfo[0].describe,
                    "difficulty": res.rangeInfo[0].difficulty,
                    "network": network,
                    "is_public": res.Range.is_public,
                    "one_touch": 1 if res.clone_range_uuid else 0
                }
            ]
        except Exception as e:
            current_app.logger.exception(e)
            abort(500, message="参数错误")
            return False

        return {"code": 200, "data": info}

    def post(self):  # sourcery skip: extract-method
        # 1、靶场详情
        # 2、判断身份，只有教师和管理员才可以创建靶场信息
        # 配额检测
        range_count = rangeModel.Range.query.filter_by(user_id=self.user.id).count()
        if range_count and range_count > current_app.config["USER_TO_OPENSTACK"][self.user.project_id]["quotas"]["rangeMax"]:
            abort(400, message="最大创建数超配额")
        parser = reqparse.RequestParser()
        parser.add_argument("name", help="靶场名不能为空，请定义name属性", required=True)
        parser.add_argument("type", type=int, help="靶场类型错误", required=True)
        parser.add_argument("task_id", type=int)
        parser.add_argument("courseware_id", type=int)
        parser.add_argument("describe", type=str)
        parser.add_argument("ctf_id", type=int)
        parser.add_argument("difficulty", type=int)
        parser.add_argument("is_public", type=bool, default=False)
        args = parser.parse_args()
        # 检测参数是否正确
        dictParm = self.checkRangeLinkModuleParm(
            _type=args["type"],
            task_id=args["task_id"],
            courseware_id=args["courseware_id"],
            ctf_id=args["ctf_id"],
        )
        parm = {dictParm: args[dictParm]}
        # 判断我的课件是否创建过靶场
        # task_id type ==1 时候先跳过校验
        if not args["task_id"] or args["type"] != 1:
            rangeLinkModuleInfo = rangeModel.RangeLinkModule.query.filter_by(
                user_id=self.user.id, **parm
            ).first()
            if rangeLinkModuleInfo:
                abort(400, message="参数错误")
        # 获取vxlan_pool
        range_pool_obj = rangeModel.RangeVni.query.first()
        if not range_pool_obj:
            abort(400, "项目初始化失败， 请重新启动项目")
        vxlan_pool_id = range_pool_obj.pool_uuid
        with db.auto_commit():
            # 插入基本新
            obj = rangeModel.Range(
                name=args["name"],
                # type=args["type"],
                difficulty=args["difficulty"],
                describe=args["describe"],
                user_id=self.user.id,
                is_public=args["is_public"],
                is_draft=True
            )
            rangeModel.db.session.add(obj)
            rangeModel.db.session.flush()
            # 插入关联表
            rangeModel_obj = rangeModel.RangeLinkModule(
                range_id=obj.id,
                user_id=self.user.id,
                vxlan_pool_id=vxlan_pool_id,
                network_id=Tools.get_uuid4_no_line(),
                **parm,
            )
            rangeModel.db.session.add(rangeModel_obj)
            rangeModel.db.session.flush()
        action_params = {
            "create_l2_vxlan_network": {
                "pool_uuid": rangeModel_obj.vxlan_pool_id,
                 "name": obj.name,
                 "zone_uuid": current_app.config["ZSTACK_CLOUD"]["zone_uuid"],
                 "resource_uuid": rangeModel_obj.network_id,
                 "state": 1
            }
        }
        create_transaction(action_params, rangeModel_obj.range_uuid)
        # 靶场定时取消
        range_expires = flask_config.RANGE_EXPIRES
        revoke_task(rangeModel_obj.range_uuid)
        add_job_to_scheduler(task_id=rangeModel_obj.range_uuid, 
                             func=clean_range_vm,
                             trigger="date",
                             args=[rangeModel_obj.range_uuid],
                             **{"run_date": Tools.get_future_time_str(range_expires), 
                                "func_name": "clean_range_vm"}
                             )
        
        return {
            "code": 200,
            "data": {
                "range_uuid": rangeModel_obj.range_uuid,
                # "type": obj.type,
                dictParm: args[dictParm],
                "name": args["name"],
                "describe": args["describe"],
                "difficulty": args["difficulty"],
            },
        }

    def put(self, range_uuid):
        if not range_uuid:
            abort(400, message="参数错误")

        parser = reqparse.RequestParser()
        parser.add_argument("name", help="靶场名称不能为空", required=True)
        parser.add_argument("describe", type=str, default="")
        parser.add_argument("difficulty", type=int, default=1)
        parser.add_argument("is_public", type=bool, default=False)
        args = parser.parse_args()

        if not all([args["name"]]):
            abort(400, message="参数错误")

        myRangeInfo = self.getMyRangeByUUID(range_uuid)
        if myRangeInfo is None:
            abort(400, message="靶场不存在")

        if (
            myRangeInfo.Range.is_public
            and not args["is_public"]
            and myRangeInfo.user_id != self.user.id
        ):
            abort(400, message="非场景创建用户不允许修改是否公开属性")

        if myRangeInfo.Range.is_system == 1:
            abort(400, message="系统模板不能修改")

        try:
            db_update = {
                    "name": args["name"],
                    "describe": args["describe"],
                    "difficulty": args["difficulty"],
                    "is_public": args["is_public"],
                    "is_draft": True,
                }
            rangeModel.Range.query.filter_by(id=myRangeInfo.range_id).update(db_update)
            db.session.commit()
            return {"code": 200, "data": "保存成功"}

        except Exception as e:
            db.session().rollback()
            current_app.logger.exception(e)
            abort(500, message="参数错误")


# 场景生产模板
class RangeStackTemplate(RangeBase):
    
    @property
    def load_cloud_object(self):
        return WebHookManager()
    
    def post(self):
        """根据场景生成资源栈"""
        parser = reqparse.RequestParser()
        parser.add_argument("range_uuid", help="场景不能为空， 请重新输入", required=True)
        parser.add_argument("ctf_name", help="模板名称不能为空, 请重新输入", required=True)
        args = parser.parse_args()

        range_uuid = args.range_uuid
        if rangeModel.StackTemplate.query.filter_by(range_uuid=range_uuid, is_system="0").all():
            abort(400, message="场景已经存在模板, 请勿重复创建")

        # 生成模板
        range_data = self._building_data(args.range_uuid)

        # 1. 二层网络
        range_model = rangeModel.RangeLinkModule.query.filter_by(range_uuid=args.range_uuid).first()
        # 构建json
        tpl_l2_pool_uuid = range_model.vxlan_pool_id
        l2_uuid = Tools.get_uuid4_no_line()
        zone_uuid = current_app.config["ZSTACK_CLOUD"]["zone_uuid"]
        pool_uuid = tpl_l2_pool_uuid

        l2_network = json.loads(l2_network_tpl.l2_network_str.format(
            l2_uuid=l2_uuid, zone_uuid=zone_uuid, pool_uuid=pool_uuid))
        basic_json = basic_tpl.basic_json
        device_json = l2_network
        router_to_vm_list = []
        for vpc_uuid, value in range_data.items():
            # 构建交换机
            # 2.交换机
            switch_obj = switchModel.Switchs.query.filter_by(range_uuid=args.range_uuid, subnet_id=vpc_uuid).first()
            vpc_cidr = switch_obj.cidr
            vpc_network = json.loads(l3_network_tpl.vpc_network_str.format(
                vpc_uuid=vpc_uuid, l2_uuid=l2_uuid))
            vpc_network_cidr = json.loads(l3_network_tpl.vpc_network_cidr.format(
                vpc_uuid=vpc_uuid, vpc_cidr=vpc_cidr))
            device_json = dict(device_json, **vpc_network, **vpc_network_cidr)
            # 路由器
            for router_num, (router_key, router_value) in enumerate(value.get("router_gw", {}).items()):
                router_offer_uuid = current_app.config["ZSTACK_CLOUD"]["router_uuid"]
                router_json, attach_router_json, router_to_vm = self._create_router_template(router_key, router_key, router_offer_uuid, router_num, vpc_uuid)
                device_json = dict(device_json, **router_json, **attach_router_json)
                router_to_vm_list.append(router_to_vm)
            # 虚拟机
            for vm_key, vm_value in value.get("vm", {}).items():
                vm_json = self._create_vm_template(vpc_uuid, vm_key, vm_value, router_to_vm_list)
                device_json = dict(device_json, **vm_json)
            # 安全组
            basic_json["Resources"] = device_json
        resource_id = Tools.get_uuid4_no_line()
        self.load_cloud_object.add_stack_template(args.range_uuid, basic_json, resource_id, range_uuid)
        # return {"code": 200, "data": resource_id}
        return {"code": 200, "data": "创建成功"}
    
    def delete(self, stack_id):
        """删除模板"""
        try:
            stack_template = rangeModel.StackTemplate.query.filter_by(id=stack_id)
            template_obj = stack_template.first()
            self.load_cloud_object.add_stack_template(template_obj.stack_uuid)
            stack_template.delete()
            db.session.commit()
        except Exception:
            db.session.rollback()
        return {"code": 200, "data": "删除成功"}
        
    
    @staticmethod
    def _building_data(range_uuid):
        # 根据场景id获取该场景下所有数据关联关系
        data= {}
        port_obj = switchModel.Ports.query.filter_by(range_uuid=range_uuid).all()
        for port in port_obj:
            subnet_id = port.subnet_id
            ip = port.ip
            device_owner = port.device_owner
            device_id = port.device_id
            basic_dict = {device_id: {"ip": ip}}
            if device_owner not in ["vm", "router_gw"]:
                continue
            if device_owner == "vm":
                range_vm_obj = rangeModel.RangeVm.query.filter_by(range_uuid=range_uuid, vm_uuid=device_id).first()
                basic_dict[device_id].update({
                    "image_uuid": range_vm_obj.image,
                    "instance_offering_uuid": range_vm_obj.flavor,
                    "vm_name": range_vm_obj.vm_name
                })
            if not data.get(subnet_id):
                data[subnet_id]= {device_owner: basic_dict}
            elif data[subnet_id].get(device_owner, {}):
                data[subnet_id][device_owner].update(basic_dict)
            else:
                data[subnet_id][device_owner] = basic_dict
        # TODO[查找是否有安全组](GW)
        # security_associate_list = rangeModel.SecurityGroupAssociate.query.filter_by(range_uuid=range_uuid).all()
        # for  security in security_associate_list:
            
        return data
    
    @staticmethod
    def _create_vm_template(vpc_uuid, vm_uuid, vm_value, router_to_vm_list, start_vm=True):
        """生成虚拟机json模板"""
        instance_offering_uuid = vm_value["instance_offering_uuid"]
        image_uuid = vm_value["image_uuid"]
        vm_json =  json.loads(
            vm_tpl.vm_str.format(
                vm_uuid=vm_uuid,
                vm_name=vm_value["vm_name"],
                instance_offering_uuid=instance_offering_uuid,
                image_uuid=image_uuid,
                vpc_uuid=vpc_uuid,
                ip=vm_value["ip"]
            )
        )
        router_to_vm_list=",".join(router_to_vm_list)
        if router_to_vm_list:
            vm_json[f"vm-{vm_uuid}"]["DependsOn"].append(router_to_vm_list)
        if not start_vm:
            vm_json[f"vm-{vm_uuid}"]["Properties"]["strategy"] = "CreateStopped"
        return vm_json  
    
    @staticmethod
    def _create_router_template(router_uuid, router_name, router_offer_uuid, router_num, vpc_uuid):
        """生成路由器json模板"""
        router_json = json.loads(router_tpl.router.format(
            router_uuid=router_uuid, 
            router_name=router_name, 
            router_offer_uuid=router_offer_uuid))
        attach_router_json = json.loads(router_tpl.attach_router_to_l3network.format(
            router_num=router_num, 
            vpc_uuid=vpc_uuid, 
            router_uuid=router_uuid))
        router_to_vm = router_tpl.attach_router_to_l3network_vm.format(router_num=router_num)
        return router_json, attach_router_json, router_to_vm

    def _create_security_group_template():
        """生成安全组模板"""
        pass
    

# 靶场信息详情接口，包括虚拟机信息
class RangeInfo(RangeBase):
    # 获取靶场信息
    def post(self):
        parser = reqparse.RequestParser()
        parser.add_argument("range_id", help="靶场信息有误", required=True)
        args = parser.parse_args()
        if args["range_id"] is None:
            abort(400, message="参数错误")
        try:
            return self._extracted_from_post(args)
        except Exception as e:
            current_app.logger.error("获取靶场信息失败", exc_info=e)
            abort(500, message="参数错误")

    def _extracted_from_post(self, args):
        myRangeInfo = self.getMyRangeByUUID(args["range_id"])
        info = {
            "list": [],
            "name": myRangeInfo.Range.name,
            "topology_json": myRangeInfo.Range.topology_json,
            "topology_img": myRangeInfo.Range.topology_img,
        }
        if myRangeInfo.vmList:
            for vm in myRangeInfo.vmList:
                info["list"].append(vm.vm_uuid)
        return {"code": 200, "data": info}


# 靶场拓扑确认保存状态
class RangeConfirm(RangeBase):
    """靶场保存确认"""

    class ErrorMsg(TextChoices):
        NOT_SWITCH_IN_TOPOLOGY = "拓扑内必须存在交换机"
        NOT_SWITCH_IN_RANGE = "拓扑内交换机未创建"
        VM_NOT_LINK_SWITCH = "拓扑内: {vm_name} 未连接到交换机"

    def check_switch(self, range_manager: RangeManager):
        """检查拓扑中是否存在交换机"""
        if not range_manager.range_model.Range.topology_json:
            return None
        topology_info = json.loads(range_manager.range_model.Range.topology_json)
        node_list = topology_info["nodeList"]
        line_list = topology_info["lineList"]

        # switch 拓扑ID 定位
        switch_node_ids = [item["id"] for item in node_list if item["type"] == "switch"]
        if not switch_node_ids:
            return self.ErrorMsg.NOT_SWITCH_IN_TOPOLOGY

        range_switch = Switchs.query.filter(
            Switchs.range_uuid == range_manager.range_model.range_uuid
        ).first()
        if not range_switch:
            return self.ErrorMsg.NOT_SWITCH_IN_RANGE

        # 获取所有交换机关联虚拟机
        switch_link_node = set()
        for item in line_list:
            if item["to"] in switch_node_ids or item["from"] in switch_node_ids:
                switch_link_node.add(item["to"])
                switch_link_node.add(item["from"])

        # 过滤未连接到交换机的虚拟机
        for item in node_list:
            if item["id"] not in switch_link_node:
                return self.ErrorMsg.VM_NOT_LINK_SWITCH.format(vm_name=item["name"])

        return None

    def get(self):
        """获取靶场绑定比赛/课程信息"""

        params = request.args
        range_uuid = params.get("range_uuid", default=None)
        if not range_uuid:
            abort(400, message="range_uuid是必要参数")

        range_manager = RangeManager(range_uuid=range_uuid)
        _type, obj = range_manager.link_type, range_manager.link_obj

        msg = f"该场景已绑定{_type.label}: {obj.name}, 请解绑后再继续操作。" if _type else ""
        check_switch_msg = self.check_switch(range_manager)
        if check_switch_msg:
            msg += f"{check_switch_msg}请创建交换机后再操作，否则学生端无法正常创建环境"

        if msg:
            is_linked = True
            msg = f"{msg}是否仍要保存？"
        else:
            is_linked = False

        return {"code": 200, "data": {"is_linked": is_linked, "msg": msg}}

    def post(self):
        parser = reqparse.RequestParser()
        parser.add_argument("range_uuid", help="靶场信息有误", required=True)
        kwargs = parser.parse_args()

        range_manager = RangeManager(range_uuid=kwargs["range_uuid"])
        _type, obj = range_manager.link_type, range_manager.link_obj

        error_msg = ""
        if _type is not None:
            error_msg += f"该场景已绑定{_type.label}: {obj.name}, 不允许修改场景任何信息，如需修改请先解除关联关系。"
        _check_msg = self.check_switch(range_manager)
        if _check_msg:
            error_msg += _check_msg

        if error_msg:
            abort(400, message=f"{error_msg}拓扑将保持草稿暂存状态")

        range_module_info = self.getMyRangeByUUID(kwargs["range_uuid"])
        range_info: rangeModel.Range = range_module_info.Range

        try:
            range_info.is_draft = False
            db.session.add(range_info)
            db.session.commit()
        except Exception as e:
            current_app.logger.error("修改靶场保存状态错误", exc_info=e)
            db.session.rollback()
            abort(400, message="修改靶场保存状态错误")
        else:
            return {"code": 200, "data": ""}


# 靶场拓扑管理
class RangeTopology(RangeBase):
    # 保存拓扑图及各虚拟设备定位信息

    def post(self):
        parser = reqparse.RequestParser()
        parser.add_argument("range_uuid", help="靶场信息有误", required=True)
        parser.add_argument("tp_json", help="靶场拓扑参数", required=True)
        parser.add_argument("topology_img", help="靶场拓扑截图", required=True)
        parser.add_argument("save_all", help="保存所有拓扑")
        args = parser.parse_args()

        if args["range_uuid"] is None and args["tp_json"] is None:
            abort(400, message="参数错误")

        tp_json = json.loads(args["tp_json"])
        if len(tp_json["nodeList"]) == 0:
            abort(400, message="空白场景无法保存")

        if "save_all" in args and args["save_all"] == "True":
            vm_name = judge_topo(tp_json)
            if vm_name:
                abort(400, message=f"请先创建{vm_name}虚拟机")

        myRangeInfo = self.getMyRangeByUUID(args["range_uuid"])
        if myRangeInfo is None:
            abort(400, message="靶场不存在")

        try:
            rangeModel.Range.query.filter_by(id=myRangeInfo.range_id).update(
                {
                    "topology_json": args["tp_json"],
                    "topology_img": args["topology_img"],
                    "is_draft": True,
                }
            )
            rangeModel.db.session.commit()
            return {"code": 200, "data": "保存成功"}

        except Exception as e:
            rangeModel.db.session().rollback()
            current_app.logger.exception(e)
            abort(500, message="参数错误")


# 靶场开启接口
class RangeStart(RangeBase):
    # 保存拓扑图及各虚拟设备定位信息
    def post(self):
        parser = reqparse.RequestParser()
        parser.add_argument("range_uuid", help="靶场信息有误", required=True)
        args = parser.parse_args()
        if not args["range_uuid"]:
            abort(400, message="参数错误")

        myRangeInfo = self.getMyRangeByUUID(args["range_uuid"])

        if myRangeInfo is None:
            abort(400, message="参数错误")

        if myRangeInfo.rangeInfo[0].start == 1:
            abort(400, message="环境已经启动")

        try:
            rangeModel.Range.query.filter_by(id=myRangeInfo.range_id).update(
                {"start": 1}
            )
            rangeModel.db.session.commit()

        except Exception as e:
            rangeModel.db.session().rollback()
            current_app.logger.exception(e)
            abort(500, message="参数错误")
        return {"code": 200, "data": "启动成功"}


# 靶场克隆接口
class RangeCollect(RangeBase):
    
    class CloneOptions(TextChoices):
        ctf_range_link = "ctf_range_link", "ctf比赛克隆场景"
        red_blue_combat_range_link = "red_blue_combat_range_link", "红蓝对抗关联场景"
        courseware_range_link = "courseware_range_link", "课件克隆场景"
        range_template_clone = "range_template_clone", "靶场模板克隆场景"

    class UnbindOptions(TextChoices):
        ctf_range_unlink = "ctf_range_unlink", "ctf比赛删除靶场"
        ctf_red_blue_unlink_range = "ctf_red_blue_unlink_range", "红蓝对抗赛取消关联靶场"
        courseware_range_unlink = "courseware_range_unlink", "课件删除靶场"

    def __init__(self, api=None, *args, **kwargs):
        super().__init__(api=api, *args, **kwargs)
        self.conn = init_openstack(self.user.project_id, current_app.config)

    def createNetwork(self, range_uuid):
        if not range_uuid:
            abort(400, message="参数错误")
        # 检测参数是否正确
        rang_link_module_info = self.getMyRangeByUUID(range_uuid=range_uuid)

        if not rang_link_module_info:
            abort(400, message="参数错误")
        net_name = f"net-{str(rang_link_module_info.id)}"

        try:
            example_network = self.conn.network.create_network(name=net_name)
        except Exception as e:
            current_app.logger.exception(e)
            abort(500, message="获取失败")
        else:
            return example_network.id

    @staticmethod
    def check_red_blue(ctf_id):
        ctf = ctfModel.CtfRedBlue.query.filter(ctfModel.CtfRedBlue.id == ctf_id)
        if ctf.count() != 1:
            abort(400, message="无法查询红蓝比赛")
        ctf = ctf.first()
        if ctf.status != ctf.StatusChoices.wait_start.value:
            abort(400, message="该操作应在比赛开始前进行")

    def create_switch(self, range_uuid, cidr, network_id):
        switch = switchModel.Switchs.query.filter_by(
            range_uuid=range_uuid, cidr=cidr
        ).first()
        if switch:
            return switch
        try:
            obj = switchModel.Switchs(
                    range_uuid=range_uuid,
                    subnet_id=Tools.get_uuid4_no_line(),
                    cidr=cidr,
                    network_id=network_id,
                    switch_name="交换机"
                )
            switchModel.db.session.add(obj)
            switchModel.db.session.flush()
            return obj
        except Exception:
            switchModel.db.session.rollback()
            abort(400, "克隆交换机失败")

    def create_router(self, range_uuid, router_old, device_id):
        print(range_uuid, router_old.binding_switchs)
        router_name = f"{range_uuid[:5]}-router"
        
        try:
            obj = switchModel.Routes(op_route=device_id, range_uuid=range_uuid, state=0)
            db.session.add(obj)
            db.session.flush()
        except Exception:
            db.session.rollback()
            abort(400, message="路由器克隆失败")

        binding_switchs = router_old.binding_switchs.split(",")
        binding_switchs_new = ""

        # 根据旧的网关ip创建新的网关ip
        for switch_id in binding_switchs:
            if not switch_id:
                continue
            switch_old = switchModel.Switchs.query.filter_by(id=int(switch_id)).first()
            switch_new = switchModel.Switchs.query.filter_by(
                cidr=switch_old.cidr, range_uuid=range_uuid
            ).first()
            if binding_switchs_new:
                binding_switchs_new += f",{str(switch_new.id)}"
            else:
                binding_switchs_new = str(switch_new.id)
            gw_ip_old = switchModel.Ports.query.filter_by(
                device_id=router_old.op_route, subnet_id=switch_old.subnet_id
            ).first()
            
            port = switchModel.Ports(
                port_id=Tools.get_uuid4_no_line(),
                range_uuid=range_uuid,
                ip=gw_ip_old.ip,
                network_id=switch_new.network_id,
                subnet_id=switch_new.subnet_id,
                device_id=device_id,
                device_owner="router_gw",
            )
            db.session.add(port)
            db.session.flush()

        # 更新路由表关联的交换机
        obj.binding_switchs = binding_switchs_new
        db.session.add(obj)
        db.session.flush()
        # 根据旧的路由表创建新的路由表
        router_table_old_list = switchModel.RouterTable.query.filter_by(
            router_id=router_old.op_route
        ).all()

        routes = []
        router_table_ob_list = []
        for route_table in router_table_old_list:
            static_route = {
                "destination": route_table.destination_cidr,
                "nexthop": route_table.next_hop,
            }
            routes.append(static_route)
            router_table_ob_list.append(
                {
                    "router_id": router_old.op_route,
                    "destination_cidr": route_table.destination_cidr,
                    "next_hop": route_table.next_hop,
                }
            )
        try:
            switchModel.db.session.bulk_insert_mappings(
                switchModel.RouterTable, router_table_ob_list
            )
            db.session.flush()
        except exceptions.BadRequestException as e:
            if "Invalid format for routes" in str(e):
                abort(400, message="路由格式无效")

    @classmethod
    def record_init_issue_bind(cls, vm_info: RangeVm):
        """记录初始系统内置赛题与虚拟机绑定关系"""
        try:
            bind_relations = [
                IssueInitialBindRelation(
                    range_uuid=vm_info.range_uuid,
                    vm_uuid=vm_info.vm_uuid,
                    issue_id=issue.id,
                )
                for issue in vm_info.bind_question
            ]
            db.session.add_all(bind_relations)
            db.session.flush()
        except Exception as e:
            current_app.logger.error("记录初始赛题与虚拟机绑定关系失败", exc_info=e)
            raise e

    def range_clone(self, range_info: RangeLinkModule, args):
        """克隆靶场"""
        router_list = switchModel.Routes.query.filter_by(
            range_uuid=args["range_uuid"]
        ).all()
        switch_list = switchModel.Switchs.query.filter_by(
            range_uuid=args["range_uuid"]
        ).all()
        # 重新生成tp_json中得port_id
        tp_json, vm_uuid_dict = update_uuid(range_info.Range.topology_json)
        filters = [rangeModel.Range.name.contains(range_info.Range.name)]
        num = rangeModel.Range.query.filter(*filters).count()
        # 基础场景
        range_obj = rangeModel.Range(
            name=f'{range_info.Range.name}_{num}',
            difficulty=range_info.Range.difficulty,
            describe=range_info.Range.describe,
            user_id=self.user.id,
            topology_json=tp_json,
            topology_img=range_info.Range.topology_img,
            range_template_type=range_info.Range.range_template_type,
            is_system=0,
        )

        db.session.add(range_obj)
        db.session.flush()
        # 获取vxlan pool
        range_vni = rangeModel.RangeVni.query.first()
        # 插入关联表
        range_model_uuid = str(uuid.uuid1())
        range_model_obj: RangeLinkModule = rangeModel.RangeLinkModule(
            range_id=range_obj.id, 
            range_uuid=range_model_uuid, 
            user_id=self.user.id,
            vxlan_pool_id=range_vni.pool_uuid, 
            network_id="",
            clone_range_uuid=args["range_uuid"]
        )
        db.session.add(range_model_obj)
        db.session.flush()

        for vm_info in range_info.vmList:
            switch_old = switchModel.Switchs.query.filter_by(
                subnet_id=vm_info.subnet_id
            ).first()
            switch_new = (
                self.create_switch(range_model_obj.range_uuid, switch_old.cidr, range_model_obj.network_id)
                if switch_old and switch_old.cidr
                else None
            )
            # 克隆 当有快照存在的时候 将image换成快照ID， 虚拟机
            try:
                obj = rangeModel.RangeVm(
                    vm_name=vm_info.vm_name,
                    type=vm_info.type,
                    vm_uuid=vm_uuid_dict[vm_info.vm_name]["vm_uuid"],
                    range_uuid=range_model_obj.range_uuid,
                    image=vm_info.image_snapshot or vm_info.image,
                    is_external=vm_info.is_external,
                    v4_fixed_ip=vm_info.v4_fixed_ip,
                    flavor=vm_info.flavor,
                    status=1,
                    user_id=self.user.id,
                    subnet_id=switch_new.subnet_id if switch_new else None,
                    flag_3d_mark=vm_info.flag_3d_mark,
                    bind_question=vm_info.bind_question,
                )
                db.session.add(obj)
                db.session.flush()
                # 创建port连接虚拟机和交换机
                port = switchModel.Ports(
                        port_id=vm_uuid_dict[vm_info.vm_name]["port_id"],
                        range_uuid=obj.range_uuid,
                        ip=vm_info.v4_fixed_ip,
                        network_id=switch_new.network_id,
                        subnet_id=obj.subnet_id,
                        device_id=obj.vm_uuid,
                        device_owner="vm"
                    )

                db.session.add(port)
                db.session.flush()
            except Exception as e:
                current_app.logger.error(f"创建虚拟机<{vm_info.vm_name}>错误", exc_info=e)
                db.session.rollback()
                raise e
            else:
                self.record_init_issue_bind(obj)

        rangeModel.Range.query.filter_by(
            id=range_model_obj.range_id, user_id=self.user.id
        ).update({"vmNum": len(range_info.vmList)})
        db.session.flush()

        # 路由和交换创建
        for switch in switch_list:
            self.create_switch(range_model_obj.range_uuid, switch.cidr, range_model_obj.network_id)
        for router_old in router_list:
            device_id = vm_uuid_dict[router_old.op_route]
            self.create_router(range_model_obj.range_uuid, router_old, device_id)

        # 保存拓扑
        new_json = update_json_router_switch(tp_json, range_model_obj.range_uuid)
        rangeModel.Range.query.filter_by(id=range_model_obj.range_id).update(
            {"topology_json": new_json}
        )
        db.session.flush()

        return {"range_model_obj": range_model_obj, "range_obj": range_obj}

    def range_template_clone(self, range_info: RangeLinkModule, **kwargs):
        """场景模版克隆"""
        clone_info = self.range_clone(range_info, kwargs)
        return {
            "code": 200,
            "data": {"range_uuid": f'{clone_info["range_model_obj"].range_uuid}'},
        }

    @staticmethod
    def red_blue_combat_range_link(range_info: RangeLinkModule, **kwargs):
        """红蓝对抗关联场景"""
        _exists = ctfModel.CtfRedBlue.query.filter(
            ctfModel.CtfRedBlue.id == kwargs["ctf_id"],
            ctfModel.CtfRedBlue.range_uuid == range_info.range_uuid,
        )
        if db.exists(_exists):
            abort(400, message="该靶场已关联其他红蓝对抗比赛")
        else:
            # 更新关联场景
            ctfModel.CtfRedBlue.query.filter_by(id=kwargs["ctf_id"]).update(
                {"range_uuid": range_info.range_uuid}
            )

            ctf: ctfModel.CtfRedBlue = ctfModel.CtfRedBlue.query.get(kwargs["ctf_id"])
            # 以下条件任意存在表示曾进行解绑操作，需清理数据
            if any(
                [
                    ctf.red_initial_score != 0,
                    ctf.blue_initial_score != 100,
                    ctf.flag_list is not None,
                    ctf.link_score is not None,
                    ctf.king is not None,
                ]
            ):
                ctf.king = None
                ctf.red_initial_score = 0
                ctf.blue_initial_score = 100
                ctf.flag_list = []
                ctf.link_score = []
                db.session.add(ctf)
                db.session.flush()

            return {
                "code": 200,
                "data": {"range_uuid": f"{range_info.range_uuid}", "is_link": True},
            }

    @staticmethod
    def ctf_range_link(range_info: RangeLinkModule, **kwargs):
        """ctf克隆场景"""
        # clone_info = self.range_clone(range_info, kwargs)
        # range_uuid = clone_info["range_model_obj"].range_uuid
        range_uuid = range_info.range_uuid
        ctfModel.Ctf.query.filter_by(id=kwargs["ctf_id"]).update(
            {"range_uuid": range_uuid}
        )
        return {"code": 200, "data": {"range_uuid": f"{range_uuid}"}}

    @staticmethod
    def courseware_range_link(range_info: RangeLinkModule, **kwargs):
        """课件场景克隆"""
        # clone_info = self.range_clone(range_info, kwargs)
        # range_uuid = clone_info["range_model_obj"].range_uuid
        # range_id = clone_info["range_obj"].id
        range_uuid = range_info.range_uuid
        range_id = range_info.Range.id
        coursewareModel.Courseware.query.filter_by(id=kwargs["courseware_id"]).update(
            {"range_uuid": range_uuid}
        )
        rangeModel.RangeLinkModule.query.filter_by(range_id=range_id).update(
            {"courseware_id": kwargs["courseware_id"]}
        )
        return {"code": 200, "data": {"range_uuid": f"{range_uuid}"}}

    def post(self):
        parser = reqparse.RequestParser()
        parser.add_argument("range_uuid", type=str, help="靶场信息有误", required=True)
        parser.add_argument("courseware_id", type=int)
        parser.add_argument("ctf_id", type=int, default=None)
        parser.add_argument("is_red_blue", type=int, default=0)
        kwargs = parser.parse_args()

        if kwargs["range_uuid"] in get_link_range_model():
            abort(400, message="该虚拟机已关联比赛或课程，不允许关联到比赛或课程")

        range_info: RangeLinkModule = rangeModel.RangeLinkModule.query.filter_by(
            range_uuid=kwargs["range_uuid"]
        ).first()
        if not range_info:
            abort(400, message="该虚拟机不存在")

        if (
            range_info.Range.topology_json is None
            or range_info.Range.topology_img is None
        ):
            abort(400, message="该虚拟机缺少拓扑，请先创建拓扑")

        if range_info.Range.is_draft:
            abort(400, message="该靶场未确认保存，请在拓扑页面确认保存")

        # 创建虚机, 超配额限制
        my_vm_count = rangeModel.RangeVm.query.filter_by(user_id=self.user.id).count()
        if my_vm_count and (
            my_vm_count
            > flask_config.USER_TO_OPENSTACK[self.user.project_id]["quotas"][
                "instances"
            ]
        ):
            abort(400, message="超出配额，请调整当前靶场虚拟资源限制")

        # 克隆选项，确定当前克隆场景，进行不同的复制/关联动作
        if kwargs["is_red_blue"] and kwargs["ctf_id"]:
            # 检查是否是已克隆的场景，比赛是否是红蓝
            clone_options = self.CloneOptions.red_blue_combat_range_link
            if range_info.Range.is_system:
                abort(400, message="无法关联场景模板")
            self.check_red_blue(kwargs["ctf_id"])
        elif kwargs["ctf_id"]:
            # 检查是否是已克隆的场景，比赛是否是ctf
            clone_options = self.CloneOptions.ctf_range_link
            if range_info.Range.is_system:
                abort(400, message="无法克隆场景模板")
            if not ctfModel.Ctf.query.get(kwargs["ctf_id"]):
                abort(400, message="ctf不存在")
        elif kwargs["courseware_id"]:
            clone_options = self.CloneOptions.courseware_range_link
            # 检查是否是已克隆的场景，检查课件是否存在
            if range_info.Range.is_system:
                abort(400, message="无法克隆场景模板")
            if not coursewareModel.Courseware.query.get(kwargs["courseware_id"]):
                abort(400, message="课件不存在")
        else:
            clone_options = self.CloneOptions.range_template_clone
            # 检查需要克隆的场景是否是is_system
            if not range_info.Range.is_system:
                abort(400, message="克隆对象非系统模板")

        with db.auto_commit():
            with db.session.begin(subtransactions=True, nested=True):
                try:
                    return getattr(self, clone_options)(range_info, **kwargs)
                except HTTPException as e:
                    db.session.rollback()
                    raise e
                except Exception as e:
                    current_app.logger.exception(e)
                    db.session.rollback()
                    abort(400, message=f"{clone_options.label} 失败")

    def delete(self):
        parser = reqparse.RequestParser()
        parser.add_argument(
            "range_uuid", type=str, help="靶场信息有误", required=True, location="args"
        )
        parser.add_argument("courseware_id", type=int, default=None, location="args")
        parser.add_argument("ctf_id", type=int, default=None, location="args")
        args = parser.parse_args()

        if args["courseware_id"] and args["ctf_id"]:
            abort(400, message="参数错误，无法确定执行动作")

        range_info = rangeModel.RangeLinkModule.query.filter_by(
            range_uuid=args["range_uuid"]
        ).first()

        if not range_info:
            abort(400, message="靶场不存在")

        unbind_option = None
        if args["courseware_id"]:
            unbind_option = self.UnbindOptions.courseware_range_unlink
        elif args["ctf_id"]:
            _exists = ctfModel.Ctf.query.filter(
                ctfModel.Ctf.id == args["ctf_id"],
                ctfModel.Ctf.range_uuid == args["range_uuid"],
            )
            if db.exists(_exists):
                unbind_option = self.UnbindOptions.ctf_range_unlink
            else:
                unbind_option = self.UnbindOptions.ctf_red_blue_unlink_range

        if not unbind_option:
            abort(400, message="无法确定执行选项")

        # 确定执行动作
        with db.auto_commit():
            db.session.begin_nested()
            try:
                # rangeModel.Range.query.filter(
                #     rangeModel.Range.id == range_info.range_id
                # ).delete()

                # 解绑课件靶场
                if unbind_option == self.UnbindOptions.courseware_range_unlink:
                    coursewareModel.Courseware.query.filter_by(
                        id=args["courseware_id"]
                    ).update({"range_uuid": None})
                # 解绑ctf靶场
                elif unbind_option == self.UnbindOptions.ctf_range_unlink:
                    ctfModel.Ctf.query.filter_by(id=args["ctf_id"]).update(
                        {"range_uuid": None}
                    )
                # 解绑红蓝对抗靶场
                elif unbind_option == self.UnbindOptions.ctf_red_blue_unlink_range:
                    # 解绑靶场仍可查看部分比赛进行信息，所以再次绑定靶场时，将进行数据清理操作
                    ctfModel.CtfRedBlue.query.filter_by(id=args["ctf_id"]).update(
                        {"range_uuid": None}
                    )
                    # 信号操作删除导入的flag
                    red_blue.delete_flag_signal.send(ctf_id=args["ctf_id"])

                # 不再删除场景
                # db.session.delete(range_info)
            except Exception as e:
                current_app.logger.exception(e)
                db.session.rollback()
                abort(400, message="操作失败")
            else:
                return {"code": 200, "data": "操作成功"}


# 靶场得分规则
class RedBlueRangeScoreInfo(RangeBase):
    def get(self, range_uuid=None):

        if not range_uuid:
            abort(400, message="range_uuid 必须提供")

        range_info = rangeModel.RangeLinkModule.query.filter_by(
            range_uuid=range_uuid
        ).first()
        if not range_info:
            abort(400, message="靶场不存在")

        _exists = ctfModel.CtfRedBlue.query.filter(
            ctfModel.CtfRedBlue.range_uuid == range_info.range_uuid
        ).exists()
        if not db.session.query(_exists).scalar():
            abort(400, message="靶场尚未关联比赛")

        preset_info = self.get_range_preset_info(range_info.range_id)

        return {
            "code": 200,
            "data": {
                "flag_info": [
                    {"describe": item.describe, "score": item.score}
                    for item in preset_info
                ],
                "message": [
                    "flag默认分数与比赛关联场景中绑定的赛题分值相同",
                    "人工裁决通过分数均为10，裁决判断比赛结束，申请方分数+99999",
                    "红队得分时，蓝队扣除对应分数",
                    "比赛关联靶场后，蓝队分数为靶场预设flag分数总和(场景内赛题分数总和)",
                ],
            },
        }


# 获取靶场内置赛题信息
class ContestInfo(RangeBase):
    def get(self):
        params = request.args
        range_uuid = params.get("range_uuid", None)

        if not range_uuid:
            abort(400, message="缺少靶场参数")

        try:
            range_manager = RangeManager(range_uuid=range_uuid)
        except PrivateRangeException as e:
            abort(400, message=e.msg)

        vm_list: List[RangeVm] = range_manager.range_model.vmList
        data = [
            {
                "question_id": val.id,
                "question_name": val.name,
                "question_desc": val.describe,
                "question_type": val.type,
                "question_score": val.score,
                "file": val.filepath,
                "vm_name": item.vm_name,
            }
            for item in vm_list
            for val in item.bind_question
        ]

        data.sort(key=lambda x: x["question_id"])

        return {"code": 200, "data": data}
